import math
import pymunk
import pyglet
from engine.graphics import draw_line_loop, draw_circle

class ServiceManager():
    """
    This class is responsible for managing game services.
    """
    instance = None
    
    def __init__(self):
        """
        Initialization method for a service manager.
        """
        self.__services = {}
        ServiceManager.instance = self
        self.broadcasts = {}
    
    def register_service(self, service_class, *args, **kwargs):
        """
        Factory method to create and register a new game service in the manager
        """
        service = service_class(*args, **kwargs)
        service.mgr = self
        self.__services[service_class] = service
        
    def add_service(self, service):
        self += service
    
    def __iadd__(self, service):
        cls = service.__class__
        if cls in self.__services:
            raise Exception("Service class %s is already registered" % cls.__name__)
        
        service.mgr = self
        self.__services[cls] = service
        return self
        
    def __getitem__(self, service_class):
        """
        Return the service by its class.
        """
        return self.__services[service_class]
    
    def get_timestamp(self):
        """
        Convenience method to get the actual timestamp of the 
        current frame.
        """
        pass
    
    def send_broadcast(self, event, *args, **kwargs):
        """
        Sends a broadcast message to all services, meaning, to call a
        function on the service if it hast it.
        The method caches what services implement the event handler for
        faster processing.
        """
        
        # cache all handlers if not present
        if event not in self.broadcasts:
            handlers = []
            for service in sorted(self.__services.values(),
                                  key = lambda service: service.priority):
                if hasattr(service, event):
                    handlers.append(getattr(service, event))
            self.broadcasts[event] = tuple(handlers)
        
        # actually 'send' the message
        for handler in self.broadcasts[event]:
            handler(*args, **kwargs)

class AbstractService(object):
    """
    An abstract base class for all game services.
    """
    
    # The priority in what order the service receives messages.
    # lower priorities mean earlier receit.
    priority = 0    

class GameObjectService(AbstractService):
    """
    GameObjectServices manage the insertion and extraction of object
    from and to the game.
    """
    
    def __init__(self):
        """
        Initializes the GameObjectService.
        """
        self.objects = []
        self.objects_to_remove = set()
    
    def add_object_class(self, cls, *args, **kwargs):
        """
        Factory method, to add a new GameObject to the game. The object
        is created by a given class and an argument list.
        """
        obj = cls(*args, **kwargs)
        return self.add_object(obj)
    
    def add_object(self, obj):
        """
        Adds a GameObject to the active GameObjects. Sends a broadcast
        message 'on_object_added' of the addition of the object to all
        other services.
        """
        obj.object_service = self
        obj.on_added()
        self.objects.append(obj)
        self.mgr.send_broadcast('on_object_added', obj)
        return obj
        
    def remove_object(self, obj):
        """
        Removes a game object from the game. Sends a broadcast message
        'on_object_removed' to all other services.
        
        UPDATE: objects are removed in the next update
        """
        self.objects_to_remove.add(obj)
        
    
    def on_tick(self, dt):
        """
        This version of on_tick sends the 'update' message to all
        active objects in the game.
        """
        for obj in self.objects_to_remove:
            obj.on_removed()
            self.objects.remove(obj)
            self.mgr.send_broadcast('on_object_removed', obj)
        self.objects_to_remove.clear()
        
        for obj in self.objects:
            obj.update(dt)

class PhysicsService(AbstractService):
    """
    The PhysicsService is responsible to hold and update the physical
    state of all objects with a physical component registered.
    It also updates the graphical position and rotation of the objects.
    """
    priority = 10
    
    def __init__(self, *args, **kwargs):
        """
        Initializes the physics engine and the PhysicsService.
        """
        pymunk.init_pymunk()
        self.space = pymunk.Space()
        self.physical_objects = []
        self.bounds = kwargs.get('bounds', None)
        
        self.space.set_default_collision_handler(self.on_collision, None, None, None)
        
        # prepare the debug drawing of a sphere
        self.debug_draw = False
        
        
    def on_tick(self, dt):
        """
        Updates the physical state of the objects in the game.
        """
        self.space.step(dt)
        for obj in self.physical_objects:
            self._check_wrap_around(obj)
            obj.sprite.position = obj.body.position
            obj.sprite.rotation = -math.degrees(obj.body.angle)
    
    def on_draw(self):
        """
        Draws all the shapes within the space.
        """
        if not self.debug_draw:
            return
        
        pyglet.gl.glColor4f(1.0, 0, 0, 1.0)
        for shape in self.space.shapes:
            if isinstance(shape, pymunk.Poly):
                draw_line_loop(shape.get_points())
            elif isinstance(shape, pymunk.Circle):
                draw_circle(shape.body.position, shape.radius)
            
    def on_object_added(self, obj):
        """
        Message handler for 'on_object_added' messages. If the object
        contains a body and a shape, it is added to the list of physical
        objects.
        """
        if hasattr(obj, 'shape') and hasattr(obj, 'body'):
            self.physical_objects.append(obj)
            self.space.add(obj.shape, obj.body)
    
    def on_object_removed(self, obj):
        """
        Message handler for 'on_object_removed' messages. Removes objects 
        and shapes from the space.
        """
        if obj in self.physical_objects:
            self.physical_objects.remove(obj)
            self.space.remove(obj.shape, obj.body)
        
    def _check_wrap_around(self, obj):
        """
        Provides the 'wrap around' functionality.
        TODO: outsource this to an own service
        maybe create a WrapAroundPhysicsService
        """
        width = self.bounds[2] - self.bounds[0]
        height = self.bounds[3] - self.bounds[1]
        if self.bounds is not None:
            while obj.body.position[0] < self.bounds[0]:
                obj.body.position[0] += width
            while obj.body.position[0] > self.bounds[2]:
                obj.body.position[0] -= width
            while obj.body.position[1] < self.bounds[1]:
                obj.body.position[1] += height
            while obj.body.position[1] > self.bounds[3]:
                obj.body.position[1] -= height
    
    def on_collision(self, space, arbiter, *args, **kwargs):
        """
        TODO document
        """
        first = arbiter.shapes[0].body.object
        second = arbiter.shapes[1].body.object
        return (first.on_collision(second, arbiter) and
                second.on_collision(first, arbiter))
    
    def switch_debug_draw(self, value):
        if value:
            self.debug_draw = not self.debug_draw

    def bbox_query(self, bbox, layers = -1, group = 0):
        query_hits = []
        def cf(_shape, data):
            shape = self.space._shapes[_shape.contents.hashid]
            query_hits.append(shape.body.obj)
        
        f = pymunk._chipmunk.cpSpaceBBQueryFunc(cf)
        pymunk._chipmunk.cpSpaceBBQuery(self.space._space,
                                        pymunk._chpmunk.cpBB(bbox),
                                        layers,
                                        group,
                                        f,
                                        None)

        return query_hits
    
    def segment_query(self, start, end, first = False, layers = -1, group = 0):
        if first:
            return self.space.segment_query_first(start, end, layers, group)
        else:
            return self.space.segment_query(start, end, layers, group)
        
    def point_query(self, point, first = False, layers = -1, group = 0):
        if first:
            return self.space.point_query_first(point, layers, group)
        else:
            return self.space.point_query(point, layers, group)
            

class GraphicsService(AbstractService):
    """
    The GraphicsService is responsible for drawing objects and managing 
    graphical representations of the game objects.
    """
    priority = 1
    
    def __init__(self):
        """
        Initializes the GraphicsService.
        """
        self.batch = pyglet.graphics.Batch()
        self.groups = {}
        self.debug_draw = False
    
    def register_display_group(self, cls, index):
        """
        Registers a new display group for a given class.
        """
        if index in self.groups:
            self.groups[index][1].append(cls)
        else:
            self.groups[index] = (OrderedGroup(index), [cls])
    
    def get_display_group(self, cls):
        """
        Accesses a display group.
        """
        for value in self.group.itervalues():
            if cls in value[1]:
                return value[0]
    
    def on_draw(self):
        """
        Draws all objects registerd in the batch.
        """
        self.batch.draw()
        
    def on_object_added(self, obj):
        """
        Adds an object to the drawing batch.
        """
        obj.sprite.batch = self.batch
        
    def on_object_removed(self, obj):
        """
        Removes an object from the drawing batch.
        """
        obj.sprite.delete()
        pass # TODO, or maybe not :)
        
class InputService(AbstractService):
    """
    Service for gathering and redirecting input signals.
    """
    def __init__(self, window):
        """
        Initializes the service and sets up the window handlers.
        """
        self.input_handlers = {}
        @window.event
        def on_key_press(key, modifiers):
            self.on_key_press(key, modifiers)
        @window.event
        def on_key_release(key, modifiers):
            self.on_key_release(key, modifiers)

    def on_key_press(self, key, modifiers):
        """
        TODO document
        """
        if key not in self.input_handlers:
            return
        
        obj, handler = self.input_handlers[key]
        if handler is not None:
            if callable(getattr(obj, handler)):
                getattr(obj, handler)(True)
            else:
                setattr(obj, handler, True)
        else:
            obj.on_key_press(key, modifiers)
    
    def on_key_release(self, key, modifiers):
        """
        TODO document
        """
        if key not in self.input_handlers:
            return
        
        obj, handler = self.input_handlers[key]
        if handler is not None:
            if callable(getattr(obj, handler)):
                getattr(obj, handler)(False)
            else:
                setattr(obj, handler, False)
        else:
            obj.on_key_release(key, modifiers)
            
    def register_input_handler(self, key, obj, handler = None):
        """
        TODO document
        """
        self.input_handlers[key] = (obj, handler)
        
class ResourceService(AbstractService):
    """
    Service for handling (loading/unloading) resources (images, sounds, 
    texts...) and managing their locations.
    """
    def __init__(self, *args, **kwargs):
        self.resources = {}
    
    def add_resource_location(self, *locations):
        pyglet.resource.path.extend(locations)
        pyglet.resource.reindex()
    
    def remove_resource_location(self, *locations):
        for location in locations:
            pyglet.resource.path.remove(location)
        pyglet.resource.reindex()
        
    def image(self, filename):
        try:
            return self.resources[filename]
        except KeyError:
            image = pyglet.resource.image(filename)
            self.resources[filename] = image
            return image
            
    def sound(self, filename):
        pass
    
class MessageService(AbstractService):
    """
    Service for a (delayed) delivery of messages between services 
    and game objects.
    """
    
    priority = 6
    
    class Message(object):
        """
        Helper class for messages.
        """
        def __init__(self, receivers, message, timestamp, *args, **kwargs):
            self.receivers = receivers
            self.message = message
            self.timestamp = timestamp
            self.args = args
            self.kwargs = kwargs
            
        def send(self):
            """
            Sends the message. i.e calls the according function with
            the saved parameters from all saved recipients.
            """
            for receiver in self.receivers:
                getattr(receiver, self.message)(*self.args, **self.kwargs)
    
    def __init__(self, *args, **kwargs):
        """
        Initializes a MessageService instance.
        """
        self.queue = []     # the main message queue of the service
    
    def send_message(self, receivers, message, delay = 0., *args, **kwargs):
        """
        Send a (delayed) message to a list of receivers.
        """
        #TODO get current timestamp
        timestamp = 0
        # check if receivers are iterable, else create a 
        # tuple with a single value
        try:
            iter(receivers)
        except TypeError:
            receivers = (receivers, )
        
        # store a new message object in the message queue 
        msg = Message(receivers, message, timestamp + delay, *args, **kwargs)
        self.queue.append(msg)
        self.queue.sort(key = lambda msg: msg.timestamp)
    
    def on_tick(self, dt):
        """
        Walk through all messages and send all which are to be dispatched.
        This should be a sorted list, so we can stop after the first one that
        has a higher timestamp than the current.
        """
        #TODO get current timestamp
        timestamp = 0
        for msg in self.queue:
            if msg.timestamp < timestamp:
                msg.send()
                self.queue.remove(msg)
            else:
                break
